// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: fengyang

#pragma once

#include <map>
#include <string>
#include <string_view>

#include <simdjson.h>

#include "coin/proto/coin_market_enums.pb.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/feed_util/book_builder.h"
#include "coin2/exchange/base/feed_util/trade.h"

namespace impl {
using coin2::exchange::base::market::ExchangeType;
using coin2::exchange::base::market::MarketType;
using coin2::exchange::base::feed::FeedUpdateManager;
using coin2::exchange::base::feed_util::Index;

class DeribitBaseFeedUpdateParser {
 public:
  static bool ParseBook(
      BookBuilder* book_builder,
      const simdjson::dom::object& obj,
      BookBuilder::BookOption book_type,
      std::string_view native_symbol,
      int64_t packet_timestamp) {
    auto exchange_timestamp = obj["timestamp"].get<int64_t>() * 1'000'000LL;  // ms -> ns
    if (exchange_timestamp < book_builder->Timestamp()) {
      SPDLOG_WARN_EVERY_NS(
          5'000'000'000L,
          exchange_timestamp,
          "{} {} {} {}, {}, {}",
          "Book of",
          native_symbol,
          "ET timestamp inverse!",
          exchange_timestamp,
          book_builder->Timestamp(),
          packet_timestamp);
      return false;
    }
    auto tx = book_builder->NewTransaction(exchange_timestamp);
    std::string_view type;
    auto err1 = obj["type"].get(type);
    if (err1 || type == "snapshot") {
      tx.Clear();
    } else if (book_type == BookBuilder::LIGHT_BBO) {
      tx.BboClear();
    }

    simdjson::dom::array asks;
    auto err2 = obj["asks"].get(asks);
    if (!err2) {
      for (const auto& pq : asks) {
        tx.UpdateAsk(pq.at(1).get<double>(), pq.at(2).get<double>());
      }
    }
    simdjson::dom::array bids;
    auto err3 = obj["bids"].get(bids);
    if (!err3) {
      for (const auto& pq : bids) {
        tx.UpdateBid(pq.at(1).get<double>(), pq.at(2).get<double>());
      }
    }
    return true;
  }

  static void ParseTrade(
      const simdjson::dom::element& trade_json,
      Trade* trade) {
    trade->timestamp = trade_json["timestamp"].get<int64_t>() * 1'000'000LL;  // ms -> ns
    trade->price = trade_json["price"].get<double>();
    trade->fill_qty = trade_json["amount"].get<double>();
    trade->side = ParseTradeSide(trade_json["direction"].get<std::string_view>());
  }

  static coin2::exchange::feed::TradeSide ParseTradeSide(
      std::string_view side) {
    if (side == "buy") {
      return coin2::exchange::feed::TRADE_BUY_SIDE;
    } else if (side == "sell") {
      return coin2::exchange::feed::TRADE_SELL_SIDE;
    } else {
      throw std::runtime_error("[Deribit] unknown side: " + std::string(side));
    }
  }

  static void ParseOpenInterest(
      const simdjson::dom::object& msg,
      OpenInterest* open_interest) {
    open_interest->timestamp = msg["timestamp"].get<int64_t>() * 1'000'000LL;
    open_interest->open_interest_qty = msg["open_interest"].get<double>();
  }

  static void ParseMarkPrice(
      const simdjson::dom::object& msg,
      MarkPrice* mark_price) {
    mark_price->timestamp = msg["timestamp"].get<int64_t>() * 1'000'000LL;
    mark_price->mark_price = msg["mark_price"].get<double>();
  }

  static void ParseIndex(
      const simdjson::dom::object& msg,
      Index* index) {
    index->timestamp = msg["timestamp"].get<int64_t>() * 1'000'000LL;
    if (msg["index_price"].error() != simdjson::NO_SUCH_FIELD) {
      index->price = msg["index_price"].get<double>();
    } else if (msg["price"].error() != simdjson::NO_SUCH_FIELD) {
      index->price = msg["price"].get<double>();
    }
  }

  static bool ParseFundingRate(
      const simdjson::dom::object& msg,
      FundingRate* funding_rate) {
    CHECK(funding_rate);

    bool is_updated = false;
    funding_rate->timestamp = msg["timestamp"].get<int64_t>() * 1'000'000LL;
    if (msg["funding_8h"].error() != simdjson::NO_SUCH_FIELD) {
      funding_rate->funding_rate = msg["funding_8h"].get<double>();
      is_updated = true;
    }
    if (msg["current_funding"].error() != simdjson::NO_SUCH_FIELD) {
      funding_rate->estimated_rate = msg["current_funding"].get<double>();
      is_updated = true;
    }
    return is_updated;
  }

  static bool ParseIvGreeks(
      const simdjson::dom::object& msg,
      IvGreeks* iv_greeks) {
    CHECK(iv_greeks);

    bool is_updated = false;
    iv_greeks->timestamp = msg["timestamp"].get<int64_t>() * 1'000'000LL;
    if (msg["ask_iv"].error() != simdjson::NO_SUCH_FIELD) {
      iv_greeks->ask_iv = msg["ask_iv"].get<double>();
      is_updated = true;
    }
    if (msg["bid_iv"].error() != simdjson::NO_SUCH_FIELD) {
      iv_greeks->bid_iv = msg["bid_iv"].get<double>();
      is_updated = true;
    }
    if (msg["mark_iv"].error() != simdjson::NO_SUCH_FIELD) {
      iv_greeks->mark_iv = msg["mark_iv"].get<double>();
      is_updated = true;
    }

    if (msg["greeks"]["delta"].error() != simdjson::NO_SUCH_FIELD) {
      iv_greeks->delta = msg["greeks"]["delta"].get<double>();
      is_updated = true;
    }
    if (msg["greeks"]["gamma"].error() != simdjson::NO_SUCH_FIELD) {
      iv_greeks->gamma = msg["greeks"]["gamma"].get<double>();
      is_updated = true;
    }
    if (msg["greeks"]["rho"].error() != simdjson::NO_SUCH_FIELD) {
      iv_greeks->rho = msg["greeks"]["rho"].get<double>();
      is_updated = true;
    }
    if (msg["greeks"]["theta"].error() != simdjson::NO_SUCH_FIELD) {
      iv_greeks->theta = msg["greeks"]["theta"].get<double>();
      is_updated = true;
    }
    if (msg["greeks"]["vega"].error() != simdjson::NO_SUCH_FIELD) {
      iv_greeks->vega = msg["greeks"]["vega"].get<double>();
      is_updated = true;
    }
    return is_updated;
  }

  static bool ParseKlines(
      const simdjson::dom::object& obj,
      Klines* klines,
      int64_t timestamp,
      fastfeed::proto::KlineInterval interval,
      bool is_reverse) {
    bool to_publish = false;
    const auto& result = obj["data"]["result"];
    const auto& ticks = result["ticks"].get<simdjson::dom::array>();
    const auto& open = result["open"].get<simdjson::dom::array>();
    const auto& high = result["high"].get<simdjson::dom::array>();
    const auto& low = result["low"].get<simdjson::dom::array>();
    const auto& close = result["close"].get<simdjson::dom::array>();
    const auto& volume = result["volume"].get<simdjson::dom::array>();
    const auto& cost = result["cost"].get<simdjson::dom::array>();
    for (int idx = 0; idx < ticks.size(); idx++) {
      int64_t open_timestamp = ticks.at(idx).get<int64_t>() * 1'000'000LL;
      auto kline_res = klines->AddKline(open_timestamp);
      if (!kline_res) continue;

      Kline* kline;
      bool freezed;
      std::tie(kline, freezed) = kline_res.value();
      kline->open_timestamp = open_timestamp;
      kline->open = open.at(idx).get<double>();
      kline->high = high.at(idx).get<double>();
      kline->low = low.at(idx).get<double>();
      kline->close = close.at(idx).get<double>();
      if (is_reverse) {
        kline->volume = cost.at(idx).get<double>();
      } else {
        kline->volume = volume.at(idx).get<double>();
      }
      kline->timestamp = timestamp;
      kline->interval = interval;

      to_publish = to_publish || freezed;
    }

    return to_publish;
  }

  static fastfeed::proto::KlineInterval ParseKlineInterval(std::string_view period) {
    static std::map<std::string, fastfeed::proto::KlineInterval> interval_map = {
      {"1", fastfeed::proto::KLINE_INTERVAL_1MIN},
      {"5", fastfeed::proto::KLINE_INTERVAL_5MIN},
      {"15", fastfeed::proto::KLINE_INTERVAL_15MIN},
      {"60", fastfeed::proto::KLINE_INTERVAL_1HOUR},
      {"1D", fastfeed::proto::KLINE_INTERVAL_1DAY},
    };
    auto iter = interval_map.find(std::string(period));
    if (iter == interval_map.end()) {
      NOTREACHED() << "Not support interval " << period;
      return fastfeed::proto::KLINE_INTERVAL_INVALID;
    }

    return iter->second;
  }
};

}  // namespace impl

using impl::DeribitBaseFeedUpdateParser;
